package com.google.code.virtualhockey.vhx.util

import scala.util.parsing.combinator.RegexParsers

/**
 * Simple CSV parser based on the following BNF grammar:
 *
 * file        ::= [ line { linebreak line } ]
 * line        ::= field ( sep field )
 * linebreak   ::= "\n" | "\r" | "\r\n"
 * field       ::= escaped | nonescaped
 * escaped     ::= ( spaces ) quote { text | linebreak | sep | doublequote } quote ( spaces )
 * nonescaped  ::= boolVal | floatVal | doubleVal | longVal | intVal
 * sep         ::= ";"
 * quote       ::= "\""
 * doublequote ::= "\"\""
 * spaces      ::= "[ \t]+"
 * text        ::= "[^;\"\n\r]"
 * boolVal     ::= "true" | "false"
 */
object CSVParser extends RegexParsers {

  // Whitespaces have special meaning in CSV
  //
  override def skipWhitespace = false

  // Literals
  //
  private def lineBreak = "\n" | "\r" | "\r\n"
  private def separator = ";"
  private def quote = "\""
  private def doubleQuote = "\"\"" ^^ { case _ => "\"" }
  private def spaces = "[ \t]+".r
  private def text = "[^;\"\n\r]".r
  private def intVal = "[\\d]+".r  ^^ { _.toInt }
  private def longVal = "[\\d]+[lL]".r  ^^ { _.toLong }
  private def doubleVal = "[\\d]*[.]?[\\d]+[dD]?".r ^^ { _.toDouble }
  private def floatVal = "[\\d]*[.]?[\\d]+[fF]".r ^^ { _.toFloat }
  private def boolVal = "true" | "false" ^^ { 
    (s) => s match {
      case "true" => true
      case _ => false
    }
  }

  // Productions
  //
  private def file: Parser[List[List[Any]]] = repsep( line, lineBreak ) <~ ( lineBreak? )
  private def line: Parser[List[Any]] = repsep( field, separator )
  private def field: Parser[Any] = escaped | nonEscaped
  private def escaped: Parser[String] = {
    ( spaces? ) ~> quote ~> ( ( text | lineBreak | separator | doubleQuote )* ) <~ quote <~ ( spaces? ) ^^ {
      case s => s.mkString
    }
  }
  private def nonEscaped: Parser[Any] = boolVal | floatVal | doubleVal | longVal | intVal

  // Helper methods
  //
  def parse( s: String ) = parseAll( file, s ) match {
    case Success( res, _ ) => res
    case e                 => throw new Exception( e.toString )
  }
}
